package uni.stuttgart.openstreetmap;

import java.util.Vector;

import android.graphics.Point;
import android.graphics.PointF;
import android.location.Location;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process;

public class TileCalculator extends HandlerThread{
	
	public static final int POSITION_MARKER_WIDTH = 64;
	public static final int POSITION_MARKER_HEIGHT = 64;
	
	public static final int UPDATE_SCROLL_POSITION = 0;
	public static final int UPDATE_GPS = 1;
	public static final int UPDATE_ZOOM = 2;
	public static final int UPDATE_MAP_MODE = 3;
	public static final int ON_LOW_MEMORY = 4;
	public static final int CALCULATE_START_END_LAT_LON = 5;
	
	public static final int MAP_MODE_FOLLOW = 0;
	public static final int MAP_MODE_SCROLL = 1;
	
	public static final int PRELOAD_BORDER_TILES_WIDTH = 1;
	
	private static Handler handler;
	private static int mapMode;
	private static int zoomLevel;
	private static UserInterface ui;
	private static TileProvider tp;
	private static int screenWidth;
	private static int screenHeight;
	private static int maxScreenSize;
	private static int numberOfScreenTilesPerDirection;
	private static Vector<Point> paintedTiles;
	private static int scrollPosX;
	private static int scrollPosY;
	
	private static final class TileCalculatorHandler extends Handler{
		TileCalculatorHandler(Looper looper){
			super(looper);
		}
		
		@Override
		public void handleMessage(Message msg) {
			switch (msg.what){
			
			case UPDATE_SCROLL_POSITION:
				updateTilesWithScrollPosition(msg.arg1,msg.arg2);
				break;
			
			case UPDATE_GPS:
				updateGPS((Location)msg.obj);
				break;
				
			case UPDATE_ZOOM:
				updateZoom(msg.arg1);
				break;
				
			case UPDATE_MAP_MODE:
				mapMode = msg.arg1;
				break;
				
			case ON_LOW_MEMORY:
				onLowMemory();
				break;
				
			case CALCULATE_START_END_LAT_LON:
				calculateStartEndCoordinates();
				break;

			default:
				break;
			}
		}
	}
	
	public void informAboutScrollChange(int x , int y){
		Message msg = TileCalculator.handler.obtainMessage(UPDATE_SCROLL_POSITION, x, y);
		TileCalculator.handler.sendMessage(msg);
	}
	
	public void informAboutGPSChange(Location location){
		Message msg = TileCalculator.handler.obtainMessage(UPDATE_GPS, location);
		TileCalculator.handler.sendMessage(msg);
	}
	
	public void informAboutZoomChange(int zoom){
		Message msg = TileCalculator.handler.obtainMessage(UPDATE_ZOOM, zoom, 0);
		TileCalculator.handler.sendMessage(msg);
	}
	
	public void informAboutMapModeChange(int mode){
		Message msg = TileCalculator.handler.obtainMessage(UPDATE_MAP_MODE, mode, 0);
		TileCalculator.handler.sendMessage(msg);
	}
	
	public void freeMemoryRequest(){
		TileCalculator.handler.sendEmptyMessage(ON_LOW_MEMORY);
	}

	public void requestStartEndLatLonCalculation(){
		TileCalculator.handler.sendEmptyMessage(CALCULATE_START_END_LAT_LON);
	}
	
	public TileCalculator(UserInterface ui, TileProvider tp, int screenWidth, int screenHeight, int zoom, int mapMode) {
		super("TileCalculatorThread",Process.THREAD_PRIORITY_BACKGROUND);
		TileCalculator.ui = ui;
		TileCalculator.tp = tp;
		
		TileCalculator.zoomLevel = zoom;
		
		TileCalculator.screenWidth = screenWidth;
		TileCalculator.screenHeight = screenHeight;
		if(screenHeight>screenWidth){
			maxScreenSize=screenHeight;
		}
		else{
			maxScreenSize=screenWidth;
		}
		numberOfScreenTilesPerDirection = maxScreenSize/MapTile.HORIZONTAL_PIXELS+2*PRELOAD_BORDER_TILES_WIDTH;

		TileCalculator.mapMode = mapMode;
		TileCalculator.paintedTiles = new Vector<Point>();
		this.start();
		handler = new TileCalculatorHandler(this.getLooper());
	}
	
	private static void updateGPS(Location location){
		Point position = calculatePositionMarkerPixelPosition(location);
		ui.requestPositionMarkerUpdate(position.x-POSITION_MARKER_WIDTH/2, position.y-POSITION_MARKER_HEIGHT/2);
		if(mapMode==MAP_MODE_FOLLOW){
			ui.requestScrollUpdate(position.x-screenWidth/2, position.y-screenHeight/2);
		}
	}
	
	private static void updateTilesWithScrollPosition(int x, int y){
		
		//Save values for zoom events 
		scrollPosX = x;
		scrollPosY = y;
		
		Point[] tiles = calculateNeededTiles(x,y);
		for(int i=0;i<tiles.length;i++){
			if(!paintedTiles.contains(tiles[i]) && tiles[i].x>=0 && tiles[i].x<(int)Math.pow(2, zoomLevel) && tiles[i].y>=0 && tiles[i].y<(int)Math.pow(2, zoomLevel)){
				tp.requestTileUpdate(new MapTile(tiles[i].x,tiles[i].y,zoomLevel,null));
				//System.out.println("Calculated Tile to add: " + tiles[i]);
				paintedTiles.add(tiles[i]);
			}
		}
		
	}
	
	private static void calculateStartEndCoordinates(){
		double startX = scrollPosX;
		double startY = scrollPosY;
		double endX = scrollPosX+screenWidth;
		double endY = scrollPosY+screenHeight;
		
		PointF startScrollPosToLatLon = tileToWorldPos(startX,startY,zoomLevel);
		PointF endScrollPosToLatLon = tileToWorldPos(endX,endY,zoomLevel);
		
		double[] startEndLatLon = new double[4];
		startEndLatLon[0] = startScrollPosToLatLon.x;
		startEndLatLon[1] = startScrollPosToLatLon.y;
		startEndLatLon[2] = endScrollPosToLatLon.x;
		startEndLatLon[3] = endScrollPosToLatLon.y;
		ui.informAboutStartEndLatLon(startEndLatLon);
	}
	
	private static void updateZoom(int zoom){
		paintedTiles = new Vector<Point>();
		System.out.println("Zoom called");
		
		double oldX = scrollPosX+screenWidth/2;
		double oldY = scrollPosY+screenHeight/2;
		
		PointF oldScrollPosToLatLon = tileToWorldPos(oldX,oldY,zoomLevel);
		zoomLevel = zoom;
		PointF newScrollPosInPixel = worldToTilePos(oldScrollPosToLatLon.x, oldScrollPosToLatLon.y, zoomLevel);
		
		System.out.println("screenWidth = " + screenWidth);
		System.out.println("screenHeight = " + screenHeight);
		
		System.out.println("scrollPosX = " + scrollPosX);
		System.out.println("ScrollPosY = " + scrollPosY);
		
		System.out.println("oldX = " + oldX);
		System.out.println("oldY = " + oldY);
		System.out.println("lat = " + oldScrollPosToLatLon.y);
		System.out.println("lon = " + oldScrollPosToLatLon.x);
		System.out.println("newX = " + (newScrollPosInPixel.x-screenWidth/2));
		System.out.println("newY = " + (newScrollPosInPixel.y-screenHeight/2));
		
		updateTilesWithScrollPosition((int)newScrollPosInPixel.x-screenWidth/2, (int)newScrollPosInPixel.y-screenHeight/2);
		
		ui.requestScrollUpdate((int)newScrollPosInPixel.x-screenWidth/2, (int)newScrollPosInPixel.y-screenHeight/2);
		
		System.out.println("Zoom end");
	}

	private static Point[] calculateNeededTiles(int x, int y){
		int startTileIndexX = x/MapTile.HORIZONTAL_PIXELS-PRELOAD_BORDER_TILES_WIDTH;
		int startTileIndexY = y/MapTile.VERTICAL_PIXELS-PRELOAD_BORDER_TILES_WIDTH;
		
		
		Point[] tiles = new Point[numberOfScreenTilesPerDirection*numberOfScreenTilesPerDirection];
		
		int w = 0;
		
		for(int i = 0;i<numberOfScreenTilesPerDirection;i++){
			for(int q =0;q<numberOfScreenTilesPerDirection;q++){
				tiles[w] = new Point(startTileIndexX+q,startTileIndexY+i);
				w++;
			}
		}
		
		return tiles;
	}
	
	private static Point calculatePositionMarkerPixelPosition(Location location){
		double lat = location.getLatitude();
		double lon = location.getLongitude();
		
		int pixelPosX;
		int pixelPosY;
		
		pixelPosX = (int)Math.round((lon + 180) / 360 * (1<<zoomLevel)*MapTile.HORIZONTAL_PIXELS);
		pixelPosY = (int)Math.round((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2 * (1<<zoomLevel)*MapTile.VERTICAL_PIXELS);
		
		Point position = new Point(pixelPosX,pixelPosY);
		return position;
	}
	
	public static PointF worldToTilePos(double lon, double lat, int zoom)
	{
		PointF p = new PointF();
		p.x = (float)((lon + 180.0) / 360.0 * (1 << zoom)*MapTile.HORIZONTAL_PIXELS);
		p.y = (float)((1.0 - Math.log(Math.tan(lat * Math.PI / 180.0) + 
			1.0 / Math.cos(lat * Math.PI / 180.0)) / Math.PI) / 2.0 * (1 << zoom)*MapTile.VERTICAL_PIXELS);
	 
	
		
		return p;
	}
	 
	public static PointF tileToWorldPos(double tile_x, double tile_y, int zoom) 
	{
		PointF p = new PointF();
		//double n = Math.PI - ((2.0 * Math.PI * tile_y) / Math.pow(2.0, zoom));
		double n = Math.PI - ((2.0 * Math.PI * tile_y) / (Math.pow(2.0, zoom)*MapTile.VERTICAL_PIXELS));
		
		
		//p.x = (float)((tile_x / Math.pow(2.0, zoom) * 360.0) - 180.0);
		p.x = (float)((tile_x / (Math.pow(2.0, zoom)*MapTile.HORIZONTAL_PIXELS) * 360.0) - 180.0);
		p.y = (float)(180.0 / Math.PI * Math.atan(Math.sinh(n)));
	 
		return p;
	}
	
	private static void onLowMemory(){
		TileCalculator.paintedTiles = null;
		TileCalculator.paintedTiles = new Vector<Point>();
	}
}
